Numerical Python, o NumPy por su abreviatura más frecuente es la lanza de batalla para la programación científica en Python puesto que presenta muchas ventajas en relación a la programación sobre Python encontrándose entre éstas rutinas para manipular grandes arreglos y matrices de datos numéricos. Algunas de los módulos contenidos en NumPy son:
Para comprender las ventajas de NumPy es importante entender porque en realidad Python es tan lento, lo cual se fundamente esencialmente en los siguientes aspectos:
1.- Python es un lenguaje escrito de forma dinámica en vez estático lo que significa que al momento en el que el programa se ejecuta, el intérprete no conoce el tipo de variable que son definidas.
2.- Python es interpretado en vez de compilado puesto que un compilador inteligente puede detectar y optimizar operaciones repetidas o no finalizadas, lo cual puede resultar en mejoras de velocidad.
3.- El Modelo de objetos de Python puede provocar ineficiencia en el acceso a la memoria en contraste con un arreglo básico de NumPy que en su forma más simple está construído sobre un arreglo de C, es decir, como un puntero hacia la dirección de memoria de cada elemento. Por otro lado, una lista de Python tiene un puntero hacia un bufer continuguo de punteros, cada uno de los cuales apuntan a un objeto de Python.
Dado lo anterior, de manera natural surge la pregunta ¿porqué entonces usar Python? y su principal ventaja corresponde justamente al hecho de que Python posee un tipeo dinámico lo que lo hace más fácil de usar que C siendo extremadamente flexible mejorando la eficientia de desarrollo de código en aquellas.
La siguiente pregunta natural entonces es: ¿Por qué NumPy es tan rápido?. La respuesta a lo anterior se fundamente esencialmente en las siguientes razones:
La estructura básica de datos en NumPy es justamente el array , el que es representado por un conjunto homogéneo de datos: int, float (por defecto), etc. que se disponen de manera adyacente en memoria unidimensional. En el caso que se necesitasen más dimensiones, NumPy proprociona la posibilidad de acceder a los elementos a través de sus índices o bien emplear la estructura matrix la cual difiere esencialmente en la definición del producto de matrices.
Para la creación de un array se poseen los siguientes mecanismos:
Junto a lo anterior, la función asarray permite transformar un objeto dado en arreglo.
In [4]:
from numpy import *
Test = [3, 4, 2]
A = array(Test)
print(A)
B = asarray(A)
print(B)
De forma similar, para realizar la operación inversa, es decir, transformar arrays a listas, se puede utilizar la función tolist . a través de la sintaxis array.tolist().
Algunas funciones útiles para operar con arrays en NumPy son las siguientes:
1.- arange, el equivalente a range en Python, pero tiene como salida un array en vez de una lista y permite utilizar espaciados de tipo float
In [5]:
Primero = arange(3)
print(Primero)
Segundo = arange(1, 10, 0.5)
print(Segundo)
2.- La función linspace nos entrega un array con un número determinado de elementos espaciados homogéneamente que incluye los límites, a diferencia de arange.
In [10]:
Linea = linspace(1, 2, 10)
print(Linea)
3.- ones Nos entrega un array con la cantidad de unos que se indique. con la opción de entregar una matriz de nos en el caso de entregar una tupla.
In [12]:
Erste = ones(3)
print(Erste)
Zweite = ones((2, 2))
print(Zweite)
4.- zeros entrega un array con ceros y opera de forma análoga al caso de ones.
In [14]:
Dritte = zeros((4, 3))
print(Dritte)
tanto en el caso de ones como zeros, la función ones_like y zeroz_like permiten crear un arreglo de igual tamaño a otro arreglo.
5.- eye genera la matriz identidad por lo que se debe indicar tanto número de filas como de columnas por lo cual permite incluso la generación de matrices no cuadradas. Es posible personalizar la diagonal que se desea llenar a través del parámetro k que indica la posición de la diagonal que se desea operar teniendo en mente que la posición por defecto, 0, corresponde a la diagonal principal.
In [18]:
Erste = eye(4,3)
Zweite = eye(2,2)
Dritte = eye(4,3, k = -1)
Vierte = eye(4,3, k = 1)
print(Erste)
print(Zweite)
print(Dritte)
print(Vierte)
6.- copy nos permite replicar un array preexistente. Para ello es importante primero comprender que cuando se asigna una variable a un array preexistente no se copia el array sino que se crea otro nombre para el mismo:
In [5]:
x = array([4, 10, 3])
y = x
print(y)
En la práctica, se puede considerar tanto a x como a y como un objeto en la memoria que mira las características del array. Así, para copiar un array, podemos ocupar el comando copy para crear otro objeto que mire al array. Es importante destacar que copy entrega los valores del array en el momento en que se copia, por lo cual si se modifica el original no lo hará copy.
In [6]:
x = array([4, 10, 3])
y = copy(x)
x[0] = 20
print(x)
print(y)
7.- concatenate((a1, a2, ..., an)) nos permite concatenar o juntar arrays de una misma dimensión.
In [12]:
Erste = array([[2, 2], [4, 2]])
Zweite = array([[1, 1]])
concatenate((Erste, Zweite))
Out[12]:
8.- logspace(n, m, k) permite producir un array con un número determinado de términos logarítmicamente espaciados donde n representa la potencia primera potencia $10^n$, m > n, la segunda potencia $10^m$ y k el número de términos y al igual que linspace, logspace también incluye los extremos del intervalo.
In [13]:
Beispiel = logspace(1, 10, 6)
print(Beispiel)
8.- mgrid[rango_x, rango_y], ( donde rango = inicio : final : espaciado ) es una poderosa función que permite la creación de arrays usados en mallados. Como resultado, mgrid nos entrega un array n-dimensional donde m[0, :, :] corresponden a las coordenadas en el eje X y m[1,:,:] las coordenadas en y, etc.
In [14]:
Beispiel = mgrid[0:10:0.1, 0:10]
print(Beispiel)
A continuación se enuncian algunas otras funciones útiles de considerar para la manipulación de arrays con NumPy:
NumPy nos permite realizar operaciones clásicas con los arreglos tanto elemento a elemento como globales. Las más comunes dentro de la clase elemento a elemento son:
Note que naturalmente se requiere que los arreglos posean las mismas dimensiones. En el caso de las funciones globales dentro de las más útiles es importante destacar:
In [20]:
def Ruido(A):
return A+3*sin(A)*log(A)
Beispiel = array([[2, 3, 4], [0.1, 0.2, 0.3], [10, 10, -1]])
Ruido(A)
Out[20]:
Sin embargo, en ocasiones dichas funciones pueden arrojarnos errores puesto que no es posible invocarlas directamente con esta clase de objetos.
In [21]:
def Comparador(A):
return A if A > 10 else -A
Beispiel = array([[2, 2, 10], [10, 4, 10]])
Comparador(A)
Lo anterior se debe a que se requiere primeramente vectorizar la función, es decir, realizar el proceso de transformación una implementación escalar a una implementación vectorial traduciéndose en el número de operaciones necesarias para operar con los elementos del array. Lo anterior se puede realizar a través de un decorador (una función que se invoca antes que nuestra función principal y la transforma). Así, usando @vectorize
In [23]:
@vectorize
def Comparador(A):
return A if A > 10 else -A
Comparador(A)
Out[23]:
Además de lo anterior, el interés de vectorizar el código se encuentra en que internamente al realizar lo anterior se eliminan índices que deben ser iterados para la operación a través de bucles sin embargo, esto produce más gasto de memoria y a veces, requiere inclusive replantear y adaptar el código. Veamos a continuación un breve ejemplo de lo anterior:
Dada una matriz $\mathcal{Q}_{m \times n}$ con $m > n$ se desea encontrar un vector ortogonal al espacio generado por las columnas de $\mathcal{Q}$.
In [28]:
def ort_escalar(Q, v):
m, n = Q.shape
for j in range(n):
v -= dot(Q[:,j], v)*Q[:,j]
return v
def ort_vectorizado(Q, v):
proyeccion = dot(Q.transpose(), v)
v -= dot(Q, proyeccion)
return v
In [29]:
m, n = 10000, 100
A = 10 * random.random((m, n))
Q, R = linalg.qr(A, mode = 'reduced')
del R
v = random.random(m)
v1 = v.copy()
v2 = v.copy()
In [30]:
%timeit ort_escalar(Q, v1)
In [31]:
%timeit ort_vectorizado(Q, v2)